﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using SuperGo.Silverlight.WebApp;
using SuperGo.SilverlightLib;
using SuperGo.SilverlightLib.History;
using System.Text.RegularExpressions;

namespace SuperGo.Silverlight {
	public partial class GamePage {
		int _boardSize = 9;
		int _gameTurnDisplayed;
		private bool _loading;
		readonly GoServicesSoapClient _service = GoService.GetGoServicesSoapClient();

		public WsGame CurrentWsGame { get; private set; }

		public GoGame CurrentGoGame { get; private set; }

		public GamePage() {
			try {
				InitializeComponent();

				Loaded += new RoutedEventHandler(Page_Loaded);
			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		private bool IsBlackLoggedIn() {
			if (CurrentWsGame == null)
				throw new Exception("Can't check if black is logged in because no game exists.");
			return !CurrentWsGame.OpponentIsBlack;
		}

		public bool LoggedInPlayerToMove() {
			GoGame goGame = CurrentGoGame;
			WsGame wsGame = CurrentWsGame;
			bool isBlackLoggedIn = IsBlackLoggedIn();

			//#if (DEBUG)
			//			return true;
			//#else
			if (goGame == null)
				return false;

			GameState gameState = goGame.GameState;

			if (gameState == null)
				return false;

			if (gameState == GameState.NegotiatingDeadStones)
				return wsGame.DateCurrentUserAcceptedDeadStones == null;

			if (gameState == GameState.BlackToMove) {
				return isBlackLoggedIn;
			}
			if (gameState == GameState.WhiteToMove) {
				return !isBlackLoggedIn;
			}

			return false;
			//#endif
		}

		public void DrawBoard(int boardSize) {
			try {
				_boardSize = boardSize;
				board.DrawBoard(boardSize, CurrentGoGame);
			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		private GoGame CreateGoGame(int size, int handicap, decimal komi, string blackPlayer, string whitePlayer) {
			GoGame goGame = new GoGame(size, handicap, komi, whitePlayer, blackPlayer);

			goGame.MoveMade += new GoGame.MoveMadeDelegate(Game_MoveMade);
			goGame.CaptureMade += new GoGame.CaptureMadeDelegate(_game_CaptureMade);
			goGame.Redraw += new GoGame.RedrawDelegate(goGame_Redraw);
			goGame.GameStateChanged += new GoGame.GameStateChangedDelegate(goGame_GameStateChanged);
			goGame.PlayerPassed += new GoGame.PlayerPassedDelegate(goGame_PlayerPassed);
			goGame.DeadStonesMarked += new GoGame.DeadStonesMarkedDelegate(goGame_DeadStonesMarked);

			return goGame;
		}

		void goGame_DeadStonesMarked(GoGame source, IList<Intersection> deadStones) {
			// if dead stones were marked while loading the game state don't send the moves to the server
			if (_loading)
				return;
			
			// get the leftmost then uppermost stone in the group and send that to the server
			Intersection upperLeftMost = deadStones.OrderBy(i => i.X).ThenBy(i => i.Y).First();
			_service.AddDeadGroupAsync(CurrentWsGame.GameId, upperLeftMost.X, upperLeftMost.Y, deadStones.Count, Page.CurrentPlayer);

			// this is a bit of a hack, but it's what the server does in the DB.  If 
			//		the opponent has already accepted dead groups this code enables the
			//		"done marking dead groups" button to pass flow back to the opponent
			//		for a second time rather than finalizing the game. 
			//		(see _gameStatus_DoneMarkingDeadGroups)
			CurrentWsGame.DateOpponentAcceptedDeadStones = null;
			CurrentWsGame.DateCurrentUserAcceptedDeadStones = null;

			CurrentGoGame.CalculateScore();
			board.Redraw();
		}

		public void StartGame(WsGame newGame) {
			try {
				if (newGame == null)
					throw new InvalidOperationException("newGame cannot be null.");
				if (newGame.GameId <= 0)
					throw new InvalidOperationException("newGame must contain a valid GameId.");

				CurrentWsGame = newGame;
				string blackPlayerName = newGame.OpponentIsBlack ? newGame.Opponent.Name : Page.CurrentPlayer.Name;
				string whitePlayerName = newGame.OpponentIsBlack ? Page.CurrentPlayer.Name : newGame.Opponent.Name;
				CurrentGoGame = CreateGoGame(newGame.BoardSize, newGame.BlackHandicap, newGame.Komi, blackPlayerName, whitePlayerName);

				DrawBoard(newGame.BoardSize);
				goGame_Redraw(CurrentGoGame);
			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		public void LoadGame(WsGame gameToLoad) {
			try {
				if (gameToLoad == null)
					throw new InvalidOperationException("newGame cannot be null.");
				if (gameToLoad.GameId <= 0)
					throw new InvalidOperationException("newGame must contain a valid GameId.");

				string blackPlayerName = gameToLoad.OpponentIsBlack ? gameToLoad.Opponent.Name : Page.CurrentPlayer.Name;
				string whitePlayerName = gameToLoad.OpponentIsBlack ? Page.CurrentPlayer.Name : gameToLoad.Opponent.Name;

				CurrentGoGame = CreateGoGame(gameToLoad.BoardSize, gameToLoad.BlackHandicap, gameToLoad.Komi, blackPlayerName, whitePlayerName);
				CurrentWsGame = gameToLoad;
				DrawBoard(gameToLoad.BoardSize);
			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		private void UpdateStatus() {
			UpdateStatus(CurrentGoGame.GameState);
		}

		private void UpdateStatus(GameState gameState) {
			_gameTurnDisplayed = CurrentGoGame.Turn - 1;
			_gameStatus.UpdateStatus(
				CurrentGoGame.Turn - 1,
				CurrentGoGame.Turn - 1,
				CurrentGoGame.BlackCaptures,
				CurrentGoGame.WhiteCaptures,
				gameState,
				LoggedInPlayerToMove()
				);
		}
		
		void goGame_GameStateChanged(GoGame source, GameState gameState) {
			UpdateStatus(gameState);

			// don't send anything to the server if we're just loading
			if (_loading) return;

			if (gameState == GameState.NegotiatingDeadStones) {
				_service.TwoPassesOccurredAsync(CurrentWsGame.GameId, Page.CurrentPlayer, Page.SessionKey);
				Page.ShowDialog("Identify Dead Groups", "Two passes have occurred so you must now identify dead groups.  Please select those groups you believe are dead and then click done.  Your opponent with then have the opportunity to confirm your selections.");
				board.Redraw();
			}
			if (gameState == GameState.WhiteResigned || gameState == GameState.BlackResigned) {
				_service.ResignAsync(CurrentWsGame.GameId, Page.CurrentPlayer, Page.SessionKey);
			}
			if (gameState == GameState.DeadStonesMergedGameOver) {
				decimal blackScore;
				decimal whiteScore;
				CurrentGoGame.CalculateScore(out blackScore, out whiteScore);
				int blackCaptures = CurrentGoGame.BlackCaptures;
				int whiteCaptures = CurrentGoGame.WhiteCaptures;

				Page.FinalizeGame(CurrentWsGame.GameId, blackScore, whiteScore, blackCaptures, whiteCaptures);
			}
		}

		void goGame_Redraw(GoGame source) {
			board.Redraw();
		}

		void Game_MoveMade(GoGame source, int x, int y, Player player, int captures) {
			try {
				// if we're loading state by replaying a game don't send moves back to the server
				if (_loading)
					return;

				// this looks odd, but this event fires after the move was made
				bool loggedInPlayerJustMoved = !LoggedInPlayerToMove();
				// ignore if we're replaying someone else's move
				if (!loggedInPlayerJustMoved)
					return;

				_service.AddMoveAsync(CurrentWsGame.GameId, x, y, player == Player.Black, captures, Page.SessionKey, Page.UserId);
				// goto: service_AddMoveCompleted
			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		public static void ParseLastMove(
			string lastMove, 
			out MoveType moveType, 
			out int? x, 
			out int? y
			) {

			x = null;
			y = null;

			if (String.IsNullOrEmpty(lastMove)) {
				moveType = MoveType.Finalize;
				return;
			}

			Match match = Regex.Match(lastMove, "[0-9]* ([MPDAT])( ([0-9]*)( ([0-9]*) ([0-9]*))?)?");
			char moveTypeChar = match.Groups[1].Captures[0].Value[0];

			switch (moveTypeChar) {
				case 'M':
					moveType = MoveType.Move;
					x = Convert.ToInt32(match.Groups[5].Captures[0].Value);
					y = Convert.ToInt32(match.Groups[6].Captures[0].Value);
					break;
				case 'P':
					moveType = MoveType.Pass;
					break;
				case 'D':
					moveType = MoveType.MarkDead;
					x = Convert.ToInt32(match.Groups[5].Captures[0].Value);
					y = Convert.ToInt32(match.Groups[6].Captures[0].Value);
					break;
				case 'A':
					moveType = MoveType.AcceptDead;
					break;
				case 'T':
					moveType = MoveType.TwoPassesOccurred;
					break;
				default:
					throw new Exception("Unknown MoveType " + lastMove[0]);
			}
		}

		public enum MoveType {
			Move,
			Pass,
			MarkDead,
			AcceptDead,
			Finalize,
			TwoPassesOccurred
		}

		void goGame_PlayerPassed(GoGame source, Player player) {
			try {
				if (_loading)
					return;

				// this looks odd, but this event fires after the move was made
				bool loggedInPlayerJustMoved = !LoggedInPlayerToMove();
				// ignore if we're replaying someone else's move
				if (!loggedInPlayerJustMoved)
					return;
	
				_service.AddPassAsync(CurrentWsGame.GameId, player == Player.Black, Page.SessionKey);

			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		protected void Page_Loaded(object sender, RoutedEventArgs e) {
			try {
				DrawBoard(_boardSize);

				_gameStatus.Pass += new EventHandler(_gameStatus_Pass);
				_gameStatus.Resign += new RoutedEventHandler(_gameStatus_Resign);
				_gameStatus.HistoryCurrent += new RoutedEventHandler(_gameStatus_HistoryCurrent);
				_gameStatus.HistoryGoBack += new RoutedEventHandler(_gameStatus_HistoryGoBack);
				_gameStatus.HistoryGoForward += new RoutedEventHandler(_gameStatus_HistoryGoForward);
				_gameStatus.HistoryFirst += new RoutedEventHandler(_gameStatus_HistoryFirst);
				_gameStatus.DoneMarkingDeadGroups += new RoutedEventHandler(_gameStatus_DoneMarkingDeadGroups);
				_gameStatus.Preferences += new RoutedEventHandler(_gameStatus_Preferences);

				_service.GetDeadGroupsCompleted += new EventHandler<GetDeadGroupsCompletedEventArgs>(service_GetDeadGroupsCompleted);
				_service.AddMoveCompleted += new EventHandler<AddMoveCompletedEventArgs>(service_AddMoveCompleted);
				_service.AddPassCompleted += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(service_AddPassCompleted);
				_service.GetLastMoveCompleted += new EventHandler<GetLastMoveCompletedEventArgs>(service_GetLastMoveCompleted);
			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		void service_AddPassCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e) {
			CurrentWsGame.CurrentMoveNumber++;
			if (CurrentGoGame.GameState != GameState.NegotiatingDeadStones)
				StartPollingForOpponentMoves();
		}

		void service_AddMoveCompleted(object sender, AddMoveCompletedEventArgs e) {
			CurrentWsGame.CurrentMoveNumber++;
			StartPollingForOpponentMoves();
		}

		private void StartPollingForOpponentMoves() {
			// after moving start polling for new moves
			_service.GetLastMoveAsync(CurrentWsGame.GameId);
			// service_GetLastMoveCompleted
		}

		void service_GetLastMoveCompleted(object sender, GetLastMoveCompletedEventArgs e) {
			string lastMove = e.Result;
			// if we see the results of our own move poll again
			if (lastMove != null && lastMove.StartsWith(Page.CurrentPlayer.PlayerId + " ")) {
				_service.GetLastMoveAsync(CurrentWsGame.GameId);
				return;
			}

			// else if we see the results of an opponents
			MoveType moveType;
			int? x;
			int? y;
			ParseLastMove(lastMove, out moveType, out x, out y);
			switch (moveType) {
				case MoveType.Move:
					CurrentWsGame.CurrentMoveNumber++;
					board.PlaceStone(x.Value, y.Value);
					break;
				case MoveType.Pass:
					CurrentWsGame.CurrentMoveNumber++;
					Page.ShowDialog("Opponent Moved", "Opponent passed.  If you pass play will end and scoring will begin.");
					Pass();
					break;
				case MoveType.TwoPassesOccurred:
					if (_previousMoveType != MoveType.TwoPassesOccurred) {
						Page.ShowDialog("Opponent Moved", "Opponent passed.  The opponent will now mark dead groups and then you will have the opportunity to approve/reject.");
					}
					_service.GetLastMoveAsync(CurrentWsGame.GameId);
					break;
				case MoveType.MarkDead:
					//Page.ShowDialog("Opponent Moved", "Opponent marked a dead group at x = " + x + " y = " + y + ".");
					_service.GetLastMoveAsync(CurrentWsGame.GameId);
					break;
				case MoveType.AcceptDead:
					Page.ShowDialog("Opponent Moved", "Opponent accepted dead stones, you must do the same to finish the game.");

					// say we're loading so identifying dead groups & pass etc doesn't send events to server
					_loading = true;

					CurrentWsGame.DateOpponentAcceptedDeadStones = DateTime.Now;
					CurrentWsGame.DateCurrentUserAcceptedDeadStones = null;

					GameState gameState = CurrentGoGame.GameState;
					if (gameState == GameState.BlackToMove || gameState == GameState.WhiteToMove) {
						// this pass occurs in AcceptDead not TwoPassesOccurred to avoid setting
						//		the game state to allow the logged in user to select dead
						Pass();
					} else {
						CurrentGoGame.ResetDeadGroups();
					}
					_service.GetDeadGroupsAsync(CurrentWsGame.GameId);
					UpdateStatus();
					break;
				case MoveType.Finalize:
					Page.ShowDialog("Game Over", "Someone won, but I won't tell who.");
					break;
				default:
					throw new Exception("Unexpected moveType " + moveType);
			}
			_previousMoveType = moveType;
		}

		private MoveType _previousMoveType;

		void _gameStatus_Preferences(object sender, RoutedEventArgs e) {
			Page.ShowPreferences();
		}

		void _gameStatus_DoneMarkingDeadGroups(object sender, RoutedEventArgs e) {
			bool opponentHasAcceptedDeadStones = CurrentWsGame.DateOpponentAcceptedDeadStones != null;
			if (opponentHasAcceptedDeadStones) {
				CurrentGoGame.MergeStonesMarkedAsDead();
				// goto goGame_GameStateChanged
			} else {
				_service.AcceptDeadGroupsAsync(CurrentWsGame.GameId, Page.CurrentPlayer, Page.SessionKey);
				Page.ShowDialog("Almost Over", "A notification has been sent to your opponent to confirm the state of the board.");
				// this is a little hacky, but it's what the server will do, and it will make the 
				//		"Done Marking Dead Groups" button invisible when UpdateStatus() is called
				CurrentWsGame.DateCurrentUserAcceptedDeadStones = DateTime.Now;
				CurrentWsGame.DateOpponentAcceptedDeadStones = null;
				UpdateStatus();

				StartPollingForOpponentMoves();
			}
		}

		void _gameStatus_Resign(object sender, RoutedEventArgs e) {
			Stone lastMove = board.GetLastMove();
			CurrentGoGame.Resign();
			if (lastMove != null)
				lastMove.Redraw(CurrentGoGame);
		}

		void _gameStatus_HistoryFirst(object sender, RoutedEventArgs e) {
			UpdateTurnDisplayed(0);
		}

		void _gameStatus_HistoryGoForward(object sender, RoutedEventArgs e) {
			if (_gameTurnDisplayed < (CurrentGoGame.Turn - 1)) {
				UpdateTurnDisplayed(_gameTurnDisplayed + 1);
			}
		}

		public void UpdateTurnDisplayed(int turnToDisplay) {
			try {
				_gameTurnDisplayed = turnToDisplay;
				HistoryEvent historyEvent = CurrentGoGame.History.GetHistoryEvent(_gameTurnDisplayed);

				_gameStatus.UpdateStatus(_gameTurnDisplayed, CurrentGoGame.Turn - 1, historyEvent.BlackCaptures, historyEvent.WhiteCaptures);
				bool inHistory = turnToDisplay != (CurrentGoGame.Turn - 1);
				board.DrawBoard(historyEvent, inHistory);
			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		void _gameStatus_HistoryGoBack(object sender, RoutedEventArgs e) {
			if (_gameTurnDisplayed > 0) {
				UpdateTurnDisplayed(_gameTurnDisplayed - 1);
			}
		}

		void _gameStatus_HistoryCurrent(object sender, RoutedEventArgs e) {
			UpdateTurnDisplayed(CurrentGoGame.Turn - 1);
		}

		protected void _gameStatus_Pass(object sender, EventArgs e) {
			Pass();
		}

		protected void Pass() {
			Stone lastMove = board.GetLastMove();
			CurrentGoGame.Pass();
			if (lastMove != null)
				lastMove.Redraw(CurrentGoGame);
		}

		protected void _game_CaptureMade(GoGame source, int x, int y) {
			board.RedrawStone(x, y, StoneState.Empty);
        }

		/// <summary>
		/// Plays out a set of moves.  This should be used in conjunction with LoadGame(), which will
		/// set up the _wsGame local variable among other things.
		/// </summary>
		/// <param name="moves">Assumption: moves are sorted ascending order by MoveNumber.</param>
		public void PlayMoves(WsMove[] moves) {
			try {
				if (CurrentWsGame == null)
					throw new Exception("LoadGame() must be called prior to PlayMoves to set the _wsGame variable.");

				// set loading to true so we don't send moves to the database
				_loading = true;

				int iteration = 0;
				foreach (WsMove move in moves) {
					// we need to determine if there was a pass by looking at the move's move number
					//		(we're assuming the moves are sorted ascending)
					while (iteration < move.MoveNumber) {
						CurrentGoGame.Pass();
						iteration++;
					}

					CurrentGoGame.Play(move.X, move.Y);
					iteration++;
				}

				// we need to determine if any of the last moves were passes
				//		by looking at the Game's CurrentMoveNumber, since passes aren't stored in
				//		the GoMove table
				while (iteration < CurrentWsGame.CurrentMoveNumber) {
					CurrentGoGame.Pass();
					iteration++;
				}

				if (CurrentGoGame.GameState == GameState.NegotiatingDeadStones) {
					_service.GetDeadGroupsAsync(CurrentWsGame.GameId);
					// goto: service_GetDeadGroupsCompleted
				} else {
					_loading = false;
					board.Redraw();
				}

				// start polling if the state is appropriate
				if (CurrentGoGame.GameState == GameState.NegotiatingDeadStones 
					|| CurrentGoGame.GameState == GameState.BlackToMove 
					|| CurrentGoGame.GameState == GameState.WhiteToMove
					) {

					if (!LoggedInPlayerToMove()) {
						StartPollingForOpponentMoves();
					}
				}
			} catch (Exception ex) {
				HandleException(ex);
			}
		}

		/// <summary>
		/// This is executed after PlayMoves()
		/// </summary>
		void service_GetDeadGroupsCompleted(object sender, GetDeadGroupsCompletedEventArgs e) {
			if (CurrentWsGame == null)
				throw new Exception("LoadGame() and PlayMoves() must be called prior to GetDeadGroupsAsync() to set the _wsGame variable.");

			WsMove[] moves = e.Result;
			foreach (WsMove move in moves) {
				CurrentGoGame.Play(move.X, move.Y);
			}
			_loading = false;
			board.Redraw();
		}
	}
}
